import pandas as pd
import xgboost as xgb
import numpy as np
from sklearn.metrics import roc_auc_score, roc_curve
import plotly.graph_objects as go
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
np.random.seed(42)
import plotly.io as pio
pio.renderers.default = "notebook"
GINI¶
Miara Giniego jest szeroko stosowana w scoringu kredytowym i analizie ryzyka. Choć często używana jako pojedyncza liczba, jej interpretacja i sens statystyczny zależą silnie od liczby obserwacji, w szczególności liczby defaultów.
0. Związek Giniego z ROC/AUC¶
$\text{Gini} = 2 \cdot \text{AUC} - 1$
1. Jak działa Gini / AUC w Pythonie¶
# y = 1 -> default (bad), y = 0 -> good
y_true = np.array([1, 1, 0, 0])
y_score = np.array([0.9, 0.8, 0.7, 0.1])
auc = roc_auc_score(y_true, y_score)
gini = 2 * auc - 1
auc, gini
(1.0, 1.0)
1️⃣ Perspektywa statystyczna – Mann–Whitney U $$AUC=\frac{U}{n_{bad}⋅n_{good}}=P(score_{bad}>score_{good})+0.5⋅P(score_{bad}=score_{good})$$
- Liczymy wszystkie pary (bad, good)
- Każda para daje: 1 jeśli bad > good, 0.5 jeśli remis, 0 jeśli bad < good
- Wynik / liczba par = AUC
To jest bezpośrednie podejście statystyczne, równoważne testowi Mann–Whitneya.
W takim razie w poprzednim przykładzie możemy AUC interpretować następująco:
Bad scores: [0.9, 0.8] Good scores: [0.7, 0.1]
Porównujemy wszystkie pary:
| bad | good | wynik |
|---|---|---|
| 0.9 | 0.7 | bad > good |
| 0.9 | 0.1 | bad > good |
| 0.8 | 0.7 | bad > good |
| 0.8 | 0.1 | bad > good |
➡️ 4/4 sukcesy ➡️ AUC = 1 ➡️ Gini = 1
#funkcja licząca manualnie wartość GINI z powyższego wzoru na AUC
def manual_auc(y_true, y_score):
bad_scores = y_score[y_true == 1]
good_scores = y_score[y_true == 0]
total = 0
count = 0
for b in bad_scores:
for g in good_scores:
if b > g:
total += 1
elif b == g:
total += 0.5
count += 1
return total / count
manual_auc(y_true, y_score)
1.0
2️⃣ Perspektywa geometryczna – pole pod krzywą ROC
Budujemy krzywą ROC: TPR vs FPR dla kolejnych progów score
Liczymy pole pod krzywą (trapezoidalna reguła numeryczna)
Wynik jest dokładnie tym samym co w punkcie 1, ale nie wymaga jawnego liczenia par ani statystyki U
ROC (Receiver Operating Characteristic) to wykres:¶
oś X: FPR (False Positive Rate) = FP / (FP + TN)
oś Y: TPR (True Positive Rate) = TP / (TP + FN)
Każdy punkt na krzywej odpowiada innemu progowi decyzyjnemu, który zamienia prawdopodobieństwa / score na klasy 0/1.
- TPR (czułość / sensitivity): jaki odsetek rzeczywiście pozytywnych przypadków model poprawnie wykrył - w przypadku banku przypadek pozytywny to bad, czyli ta definicja powinna brzmieć: odsetek dobrze zaklasyfikowanych bad.
- FPR (1 – specyficzność): jaki odsetek negatywnych przypadków model błędnie uznał za pozytywne - w przypadku banku przypadek pozytywny to bad czyli ta definicja powinna brzmieć: odsetek good błędnie zaklasyfikowanych jako bad.
Idealny klasyfikator – lewy górny róg (0, 1)¶
Lewy górny róg wykresu ROC to punkt:
- FPR = 0 → brak fałszywych alarmów
- TPR = 1 → wykryliśmy wszystkie pozytywne przypadki
Czyli: „Model wykrywa wszystko, co powinien, i nie myli się ani razu”. To jest ideał, do którego dążymy. Chcemy jednocześnie: wysokiej czułości (TPR ↑), aby nie przegapić pozytywnych przypadków oraz niskiego FPR (FPR ↓), aby nie generować fałszywych alarmów. Krzywa ROC pokazuje wszystkie możliwe progi decyzyjne: każdy punkt to inny kompromis między TPR i FPR. Dobra krzywa ROC oznacza, że istnieje próg, dla którego TPR jest wysoki przy czym FPR jest niski.Im bardziej krzywa „odgina się” w stronę lewego górnego rogu, tym lepiej model oddziela klasy.
- Przekątna (linia losowa) - krzywa ROC blisko przekątnej (od (0,0) do (1,1)), wtedy TPR ≈ FPR, czyli model działa jak losowanie, AUC ≈ 0.5.
- Prawy górny róg - TPR wysoki, ale FPR też wysoki, czyli model „łapie wszystko”, ale kosztem wielu fałszywych alarmów np. spam filtr, który oznacza prawie każdy mail jako spam
- Lewy dolny róg - FPR niski, ale TPR też niski, czyli model jest bardzo ostrożny, ale prawie nic nie wykrywa.
Jak rysuje się krzywą ROC¶
Ogólna metoda:
- Weź wszystkie unikalne wartości score (od największego do najmniejszego) jako progi.
- Dla każdego progu:
- przewiduj 1 jeśli score ≥ próg, inaczej 0
- policz TP, FP, TN, FN
- oblicz TPR = TP / (TP + FN)
- oblicz FPR = FP / (FP + TN)
- Umieść punkt (FPR, TPR) na wykresie
- Połącz punkty linią → to jest krzywa ROC
#funkcja do rysowania macierzy pomyłek
def print_c_m(thr, y_score, y_true, tpr, fpr):
print(f'cut-off: {thr}')
print(f'predykcje: {y_score}')
print(f'predykcje z obcięciem do cutoff: {(y_score >= thr).astype(int)}')
print(f'wartość prawdziwa: {y_true}')
print(f'Ture Positive Rate: {tpr}')
print(f'False Positive Rate: {fpr}')
cm = confusion_matrix(y_true, y_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['0 - negative', '1 - positive'])
disp.plot()
return plt.show()
# Przykładowe dane
y_true = np.array([1, 1, 0, 1, 0, 1, 0, 0, 1])
y_score = np.array([0.9, 0.8, 0.7, 0.7, 0.1, 0.65, 0.2, 0.3, 0.85])
# Unikalne score'y jako progi - sortowanie od największy SCORE do najmniejszy SCORE
thresholds = np.sort(np.unique(y_score))[::-1]
tpr_list = []
fpr_list = []
for thr in thresholds:
y_pred = (y_score >= thr).astype(int)
TP = np.sum((y_true == 1) & (y_pred == 1))
FP = np.sum((y_true == 0) & (y_pred == 1))
FN = np.sum((y_true == 1) & (y_pred == 0))
TN = np.sum((y_true == 0) & (y_pred == 0))
TPR = TP / (TP + FN)
FPR = FP / (FP + TN)
tpr_list.append(TPR)
fpr_list.append(FPR)
print(f'TP: {TP}, FP: {FP}, FN: {FN}, TN: {TN}')
print_c_m(thr, y_score, y_true, TPR, FPR)
# Dodaj punkt (0,0) i (1,1) żeby zamknąć krzywą
tpr_list = [0] + tpr_list + [1]
fpr_list = [0] + fpr_list + [1]
# Wykres ROC
plt.figure(figsize=(6,6))
plt.plot(fpr_list, tpr_list, marker='o', color='blue', label='ROC curve')
plt.plot([0,1], [0,1], color='grey', linestyle='--', label='Random')
plt.xlabel('False Positive Rate (1- Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.title('ROC curve - manual calculation')
plt.grid(True)
plt.legend()
plt.show()
TP: 1, FP: 0, FN: 4, TN: 4 cut-off: 0.9 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 0 0 0 0 0 0 0 0] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 0.2 False Positive Rate: 0.0
TP: 2, FP: 0, FN: 3, TN: 4 cut-off: 0.85 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 0 0 0 0 0 0 0 1] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 0.4 False Positive Rate: 0.0
TP: 3, FP: 0, FN: 2, TN: 4 cut-off: 0.8 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 1 0 0 0 0 0 0 1] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 0.6 False Positive Rate: 0.0
TP: 4, FP: 1, FN: 1, TN: 3 cut-off: 0.7 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 1 1 1 0 0 0 0 1] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 0.8 False Positive Rate: 0.25
TP: 5, FP: 1, FN: 0, TN: 3 cut-off: 0.65 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 1 1 1 0 1 0 0 1] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 1.0 False Positive Rate: 0.25
TP: 5, FP: 2, FN: 0, TN: 2 cut-off: 0.3 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 1 1 1 0 1 0 1 1] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 1.0 False Positive Rate: 0.5
TP: 5, FP: 3, FN: 0, TN: 1 cut-off: 0.2 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 1 1 1 0 1 1 1 1] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 1.0 False Positive Rate: 0.75
TP: 5, FP: 4, FN: 0, TN: 0 cut-off: 0.1 predykcje: [0.9 0.8 0.7 0.7 0.1 0.65 0.2 0.3 0.85] predykcje z obcięciem do cutoff: [1 1 1 1 1 1 1 1 1] wartość prawdziwa: [1 1 0 1 0 1 0 0 1] Ture Positive Rate: 1.0 False Positive Rate: 1.0
✅ Kluczowe
- Obie metody dały dokładnie tę samą wartość AUC
- W scikit‑learn roc_auc_score używa drugiego sposobu (krzywa ROC + pole trapezoidów), ale wynik jest matematycznie równoważny U-statystyce Mann–Whitneya
- Różnica jest tylko w sposobie obliczeń, nie w wyniku.
Symulacje wartości GINI oraz błędu (odchylenia standardowego) dla różnych wielkości zbioru BAD¶
Założenia:
- dla stałego ODR (na poziomie 2%) generowane są zestawy wartości BAD oraz GOOD, przy czym wartości BAD generowane są od 1 do 800 co 5 i do tego dobierane są wartości GOOD aby zachować proporcję ODR,
- dla każdej pary BAD-GOOD symulowane są wartości SCORE, które mogą przyjmować z rozkładu normalnego,
- SCORE dla obserwacji BAD jest z rozkładu normalnego N(signal, 1). Wartość signal czyli średniej powinna być ustawiona np. na -2 tak, aby rokład obserwacji BAD był przesunięty w lewą stronę.
- SCORE dla obserwacji GOOD jest z rokładu normalnego N(0,1).
#przykładowy wygenerowany rozkład SCORE dla liczby BAD = 200 i liczby GOOD = 800
bad_scores = np.random.normal(loc=-2, scale=1, size=200)
good_scores = np.random.normal(loc=0, scale=1, size=800)
fig = go.Figure()
# Histogram bad
fig.add_trace(go.Histogram(x=bad_scores, nbinsx=15, name='Bad (default)', opacity=0.6, marker_color='red'))
# Histogram good
fig.add_trace(go.Histogram(x=good_scores, nbinsx=15, name='Good', opacity=0.6, marker_color='blue'))
fig.update_layout(
title='Rozkład score dla Good i Bad',
xaxis_title='Score',
yaxis_title='Liczba obserwacji',
barmode='overlay', # nakładanie się histogramów
template='plotly_white')
fig.show()
#funkcja do symulowania wartości SCORE dla określonej liczby GOOD i BAD
def simulate_scores(n_bad, n_good, signal=0.5):
bad_scores = np.random.normal(loc=signal, scale=1, size=n_bad)
good_scores = np.random.normal(loc=0, scale=1, size=n_good)
y_true = np.concatenate([np.ones(n_bad), np.zeros(n_good)])
y_score = np.concatenate([bad_scores, good_scores])
return y_true, y_score
def simulate_gini(n_bad, n_good, signal=0.5):
y_true, y_score = simulate_scores(n_bad, n_good, signal)
auc = roc_auc_score(y_true, y_score)
return 2 * auc - 1
ODR = 0.02 #2%
#wyliczenie liczby good tak, żeby zachować zawsze ODR=2%
#x/(x+y) = ODR
#x=ODR*(x+y)
#x=ODR*x + ODR*y
#x-ODR*x = ODR*y
#y = x*(1-ODR)/ODR
#gdzie x=x
bads = []
goods = []
gini_val = []
gini_std = []
for b in range(1, 800, 5):
g = int(round(b*(1-ODR)/ODR, 0))
bads.append(b)
goods.append(g)
print(f'BADS: {b}, GOOD: {g} -> ODR: {round(b/g, 2)}')
ginis_simulations = [simulate_gini(b, g, signal=-2) for _ in range(100)] #symulujemy wartość SCORE dla GOOD i BAD 100 razy
gini_val.append(-1*np.mean(ginis_simulations)) #symulowaliśmy wartości SCORE a nie PD - stąd błędny kierunek i wymóg minusa
gini_std.append(np.std(ginis_simulations))
BADS: 1, GOOD: 49 -> ODR: 0.02 BADS: 6, GOOD: 294 -> ODR: 0.02 BADS: 11, GOOD: 539 -> ODR: 0.02 BADS: 16, GOOD: 784 -> ODR: 0.02 BADS: 21, GOOD: 1029 -> ODR: 0.02 BADS: 26, GOOD: 1274 -> ODR: 0.02 BADS: 31, GOOD: 1519 -> ODR: 0.02 BADS: 36, GOOD: 1764 -> ODR: 0.02 BADS: 41, GOOD: 2009 -> ODR: 0.02 BADS: 46, GOOD: 2254 -> ODR: 0.02 BADS: 51, GOOD: 2499 -> ODR: 0.02 BADS: 56, GOOD: 2744 -> ODR: 0.02 BADS: 61, GOOD: 2989 -> ODR: 0.02 BADS: 66, GOOD: 3234 -> ODR: 0.02 BADS: 71, GOOD: 3479 -> ODR: 0.02 BADS: 76, GOOD: 3724 -> ODR: 0.02 BADS: 81, GOOD: 3969 -> ODR: 0.02 BADS: 86, GOOD: 4214 -> ODR: 0.02 BADS: 91, GOOD: 4459 -> ODR: 0.02 BADS: 96, GOOD: 4704 -> ODR: 0.02 BADS: 101, GOOD: 4949 -> ODR: 0.02 BADS: 106, GOOD: 5194 -> ODR: 0.02 BADS: 111, GOOD: 5439 -> ODR: 0.02 BADS: 116, GOOD: 5684 -> ODR: 0.02 BADS: 121, GOOD: 5929 -> ODR: 0.02 BADS: 126, GOOD: 6174 -> ODR: 0.02 BADS: 131, GOOD: 6419 -> ODR: 0.02 BADS: 136, GOOD: 6664 -> ODR: 0.02 BADS: 141, GOOD: 6909 -> ODR: 0.02 BADS: 146, GOOD: 7154 -> ODR: 0.02 BADS: 151, GOOD: 7399 -> ODR: 0.02 BADS: 156, GOOD: 7644 -> ODR: 0.02 BADS: 161, GOOD: 7889 -> ODR: 0.02 BADS: 166, GOOD: 8134 -> ODR: 0.02 BADS: 171, GOOD: 8379 -> ODR: 0.02 BADS: 176, GOOD: 8624 -> ODR: 0.02 BADS: 181, GOOD: 8869 -> ODR: 0.02 BADS: 186, GOOD: 9114 -> ODR: 0.02 BADS: 191, GOOD: 9359 -> ODR: 0.02 BADS: 196, GOOD: 9604 -> ODR: 0.02 BADS: 201, GOOD: 9849 -> ODR: 0.02 BADS: 206, GOOD: 10094 -> ODR: 0.02 BADS: 211, GOOD: 10339 -> ODR: 0.02 BADS: 216, GOOD: 10584 -> ODR: 0.02 BADS: 221, GOOD: 10829 -> ODR: 0.02 BADS: 226, GOOD: 11074 -> ODR: 0.02 BADS: 231, GOOD: 11319 -> ODR: 0.02 BADS: 236, GOOD: 11564 -> ODR: 0.02 BADS: 241, GOOD: 11809 -> ODR: 0.02 BADS: 246, GOOD: 12054 -> ODR: 0.02 BADS: 251, GOOD: 12299 -> ODR: 0.02 BADS: 256, GOOD: 12544 -> ODR: 0.02 BADS: 261, GOOD: 12789 -> ODR: 0.02 BADS: 266, GOOD: 13034 -> ODR: 0.02 BADS: 271, GOOD: 13279 -> ODR: 0.02 BADS: 276, GOOD: 13524 -> ODR: 0.02 BADS: 281, GOOD: 13769 -> ODR: 0.02 BADS: 286, GOOD: 14014 -> ODR: 0.02 BADS: 291, GOOD: 14259 -> ODR: 0.02 BADS: 296, GOOD: 14504 -> ODR: 0.02 BADS: 301, GOOD: 14749 -> ODR: 0.02 BADS: 306, GOOD: 14994 -> ODR: 0.02 BADS: 311, GOOD: 15239 -> ODR: 0.02 BADS: 316, GOOD: 15484 -> ODR: 0.02 BADS: 321, GOOD: 15729 -> ODR: 0.02 BADS: 326, GOOD: 15974 -> ODR: 0.02 BADS: 331, GOOD: 16219 -> ODR: 0.02 BADS: 336, GOOD: 16464 -> ODR: 0.02 BADS: 341, GOOD: 16709 -> ODR: 0.02 BADS: 346, GOOD: 16954 -> ODR: 0.02 BADS: 351, GOOD: 17199 -> ODR: 0.02 BADS: 356, GOOD: 17444 -> ODR: 0.02 BADS: 361, GOOD: 17689 -> ODR: 0.02 BADS: 366, GOOD: 17934 -> ODR: 0.02 BADS: 371, GOOD: 18179 -> ODR: 0.02 BADS: 376, GOOD: 18424 -> ODR: 0.02 BADS: 381, GOOD: 18669 -> ODR: 0.02 BADS: 386, GOOD: 18914 -> ODR: 0.02 BADS: 391, GOOD: 19159 -> ODR: 0.02 BADS: 396, GOOD: 19404 -> ODR: 0.02 BADS: 401, GOOD: 19649 -> ODR: 0.02 BADS: 406, GOOD: 19894 -> ODR: 0.02 BADS: 411, GOOD: 20139 -> ODR: 0.02 BADS: 416, GOOD: 20384 -> ODR: 0.02 BADS: 421, GOOD: 20629 -> ODR: 0.02 BADS: 426, GOOD: 20874 -> ODR: 0.02 BADS: 431, GOOD: 21119 -> ODR: 0.02 BADS: 436, GOOD: 21364 -> ODR: 0.02 BADS: 441, GOOD: 21609 -> ODR: 0.02 BADS: 446, GOOD: 21854 -> ODR: 0.02 BADS: 451, GOOD: 22099 -> ODR: 0.02 BADS: 456, GOOD: 22344 -> ODR: 0.02 BADS: 461, GOOD: 22589 -> ODR: 0.02 BADS: 466, GOOD: 22834 -> ODR: 0.02 BADS: 471, GOOD: 23079 -> ODR: 0.02 BADS: 476, GOOD: 23324 -> ODR: 0.02 BADS: 481, GOOD: 23569 -> ODR: 0.02 BADS: 486, GOOD: 23814 -> ODR: 0.02 BADS: 491, GOOD: 24059 -> ODR: 0.02 BADS: 496, GOOD: 24304 -> ODR: 0.02 BADS: 501, GOOD: 24549 -> ODR: 0.02 BADS: 506, GOOD: 24794 -> ODR: 0.02 BADS: 511, GOOD: 25039 -> ODR: 0.02 BADS: 516, GOOD: 25284 -> ODR: 0.02 BADS: 521, GOOD: 25529 -> ODR: 0.02 BADS: 526, GOOD: 25774 -> ODR: 0.02 BADS: 531, GOOD: 26019 -> ODR: 0.02 BADS: 536, GOOD: 26264 -> ODR: 0.02 BADS: 541, GOOD: 26509 -> ODR: 0.02 BADS: 546, GOOD: 26754 -> ODR: 0.02 BADS: 551, GOOD: 26999 -> ODR: 0.02 BADS: 556, GOOD: 27244 -> ODR: 0.02 BADS: 561, GOOD: 27489 -> ODR: 0.02 BADS: 566, GOOD: 27734 -> ODR: 0.02 BADS: 571, GOOD: 27979 -> ODR: 0.02 BADS: 576, GOOD: 28224 -> ODR: 0.02 BADS: 581, GOOD: 28469 -> ODR: 0.02 BADS: 586, GOOD: 28714 -> ODR: 0.02 BADS: 591, GOOD: 28959 -> ODR: 0.02 BADS: 596, GOOD: 29204 -> ODR: 0.02 BADS: 601, GOOD: 29449 -> ODR: 0.02 BADS: 606, GOOD: 29694 -> ODR: 0.02 BADS: 611, GOOD: 29939 -> ODR: 0.02 BADS: 616, GOOD: 30184 -> ODR: 0.02 BADS: 621, GOOD: 30429 -> ODR: 0.02 BADS: 626, GOOD: 30674 -> ODR: 0.02 BADS: 631, GOOD: 30919 -> ODR: 0.02 BADS: 636, GOOD: 31164 -> ODR: 0.02 BADS: 641, GOOD: 31409 -> ODR: 0.02 BADS: 646, GOOD: 31654 -> ODR: 0.02 BADS: 651, GOOD: 31899 -> ODR: 0.02 BADS: 656, GOOD: 32144 -> ODR: 0.02 BADS: 661, GOOD: 32389 -> ODR: 0.02 BADS: 666, GOOD: 32634 -> ODR: 0.02 BADS: 671, GOOD: 32879 -> ODR: 0.02 BADS: 676, GOOD: 33124 -> ODR: 0.02 BADS: 681, GOOD: 33369 -> ODR: 0.02 BADS: 686, GOOD: 33614 -> ODR: 0.02 BADS: 691, GOOD: 33859 -> ODR: 0.02 BADS: 696, GOOD: 34104 -> ODR: 0.02 BADS: 701, GOOD: 34349 -> ODR: 0.02 BADS: 706, GOOD: 34594 -> ODR: 0.02 BADS: 711, GOOD: 34839 -> ODR: 0.02 BADS: 716, GOOD: 35084 -> ODR: 0.02 BADS: 721, GOOD: 35329 -> ODR: 0.02 BADS: 726, GOOD: 35574 -> ODR: 0.02 BADS: 731, GOOD: 35819 -> ODR: 0.02 BADS: 736, GOOD: 36064 -> ODR: 0.02 BADS: 741, GOOD: 36309 -> ODR: 0.02 BADS: 746, GOOD: 36554 -> ODR: 0.02 BADS: 751, GOOD: 36799 -> ODR: 0.02 BADS: 756, GOOD: 37044 -> ODR: 0.02 BADS: 761, GOOD: 37289 -> ODR: 0.02 BADS: 766, GOOD: 37534 -> ODR: 0.02 BADS: 771, GOOD: 37779 -> ODR: 0.02 BADS: 776, GOOD: 38024 -> ODR: 0.02 BADS: 781, GOOD: 38269 -> ODR: 0.02 BADS: 786, GOOD: 38514 -> ODR: 0.02 BADS: 791, GOOD: 38759 -> ODR: 0.02 BADS: 796, GOOD: 39004 -> ODR: 0.02
#GINI wartość w zależności od liczby BAD
fig = go.Figure()
fig.add_trace(go.Scatter(x=bads, y=gini_val, mode='lines+markers', name='Gini', line=dict(color='blue', width=2), marker=dict(size=8)))
fig.update_layout(
title='Wpływ liczby defaultów na wartość Giniego',
xaxis_title='Liczba defaultów',
yaxis_title='Gini (%)',
yaxis=dict(tickformat='.1%'),
template='plotly_white')
fig.show()
#GINI odchylenie standardowe w zależności od liczby BAD
fig = go.Figure()
fig.add_trace(go.Scatter(x=bads, y=gini_std, mode='lines+markers', name='Gini', line=dict(color='red', width=2), marker=dict(size=8)))
fig.update_layout(
title='Wpływ liczby defaultów na odchylenie standardowe wartości Giniego',
xaxis_title='Liczba defaultów',
yaxis_title='STD',
template='plotly_white')
fig.show()
n_bad_s = 10
y_true_small, y_score_small = simulate_scores(n_bad=n_bad_s,n_good=400)
auc_s = roc_auc_score(y_true=y_true_small, y_score=y_score_small)
fpr_s, tpr_s, _ = roc_curve(y_true_small, y_score_small)
auc_s = roc_auc_score(y_true_small, y_score_small)
n_bad_l = 100
y_true_large, y_score_large = simulate_scores(n_bad=n_bad_l,n_good=400)
auc_l = roc_auc_score(y_true=y_true_large, y_score=y_score_large)
fpr_l, tpr_l, _ = roc_curve(y_true_large, y_score_large)
auc_l = roc_auc_score(y_true_large, y_score_large)
fig = go.Figure()
# Krzywa ROC
fig.add_trace(go.Scatter(x=fpr_s, y=tpr_s, mode='lines+markers', name=f'ROC (n_bads = {n_bad_s}, AUC = {auc_s:.3f})',
line=dict(width=2)))
fig.add_trace(go.Scatter(x=fpr_l, y=tpr_l, mode='lines+markers', name=f'ROC (n_bads = {n_bad_l}, AUC = {auc_l:.3f})',
line=dict(width=2)))
# Linia losowa
fig.add_trace(go.Scatter(x=[0, 1], y=[0, 1], mode='lines', name='Random classifier', line=dict(dash='dash', color='gray')))
fig.update_layout(
title='Krzywa ROC (TPR vs FPR)',
xaxis_title='False Positive Rate (FPR)',
yaxis_title='True Positive Rate (TPR)',
xaxis=dict(range=[0, 1.01]),
yaxis=dict(range=[0, 1.1]),
template='plotly_white')
fig.show()
# Wyliczenie ROC manualnie dla małej liczby 10 BAD - widać lepiej przyczynę schodków
y_true = y_true_small.copy()
y_score = y_score_small.copy()
# Unikalne score'y jako progi - sortowanie od największy SCORE do najmniejszy SCORE
thresholds = np.sort(np.unique(y_score))[::-1]
tpr_list = []
fpr_list = []
licznik_podzialow = 0
for thr in thresholds:
y_pred = (y_score >= thr).astype(int)
TP = np.sum((y_true == 1) & (y_pred == 1))
FP = np.sum((y_true == 0) & (y_pred == 1))
FN = np.sum((y_true == 1) & (y_pred == 0))
TN = np.sum((y_true == 0) & (y_pred == 0))
TPR = TP / (TP + FN)
FPR = FP / (FP + TN)
tpr_list.append(TPR)
fpr_list.append(FPR)
licznik_podzialow += 1
print(f'Podział nr: {licznik_podzialow}')
print(f'TP: {TP}, FP: {FP}, FN: {FN}, TN: {TN}')
print(f'TPR: {TPR}, FPR: {FPR}')
#print_c_m(thr, y_score, y_true, TPR, FPR)
# Dodaj punkt (0,0) i (1,1) żeby zamknąć krzywą
tpr_list = [0] + tpr_list + [1]
fpr_list = [0] + fpr_list + [1]
# Wykres ROC
plt.figure(figsize=(6,6))
plt.plot(fpr_list, tpr_list, marker='o', color='blue', label='ROC curve')
plt.plot([0,1], [0,1], color='grey', linestyle='--', label='Random')
plt.xlabel('False Positive Rate (1- Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.title('ROC curve - manual calculation')
plt.grid(True)
plt.legend()
plt.show()
# Wyliczenie ROC manualnie dla dużej liczby BAD - widać lepiej przyczynę schodków
y_true = y_true_large.copy()
y_score = y_score_large.copy()
# Unikalne score'y jako progi - sortowanie od największy SCORE do najmniejszy SCORE
thresholds = np.sort(np.unique(y_score))[::-1]
tpr_list = []
fpr_list = []
licznik_podzialow = 0
for thr in thresholds:
y_pred = (y_score >= thr).astype(int)
TP = np.sum((y_true == 1) & (y_pred == 1))
FP = np.sum((y_true == 0) & (y_pred == 1))
FN = np.sum((y_true == 1) & (y_pred == 0))
TN = np.sum((y_true == 0) & (y_pred == 0))
TPR = TP / (TP + FN)
FPR = FP / (FP + TN)
tpr_list.append(TPR)
fpr_list.append(FPR)
licznik_podzialow += 1
print(f'Podział nr: {licznik_podzialow}')
print(f'TP: {TP}, FP: {FP}, FN: {FN}, TN: {TN}')
print(f'TPR: {TPR}, FPR: {FPR}')
#print_c_m(thr, y_score, y_true, TPR, FPR)
# Dodaj punkt (0,0) i (1,1) żeby zamknąć krzywą
tpr_list = [0] + tpr_list + [1]
fpr_list = [0] + fpr_list + [1]
# Wykres ROC
plt.figure(figsize=(6,6))
plt.plot(fpr_list, tpr_list, marker='o', color='blue', label='ROC curve')
plt.plot([0,1], [0,1], color='grey', linestyle='--', label='Random')
plt.xlabel('False Positive Rate (1- Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.title('ROC curve - manual calculation')
plt.grid(True)
plt.legend()
plt.show()
Wartośc GINI w porównaniu do rozkładu GOOD BAD¶
#funkcja do wyliczania rozkładów gęstości dla good i bad
from scipy.stats import gaussian_kde
def kde_from_scores(
y_true,
y_score,
good_value=0,
bad_value=1,
n_points=500,
bandwidth=None
):
"""
Liczy KDE (PDF) score osobno dla good i bad.
Parametry
----------
y_true : array-like
Wartości rzeczywiste (0/1)
y_score : array-like
Score / predykcja modelu
good_value : int
Wartość oznaczająca klasę 'good'
bad_value : int
Wartość oznaczająca klasę 'bad'
n_points : int
Liczba punktów siatki X
bandwidth : float lub None
Bandwidth dla KDE (None = automatyczny)
Zwraca
-------
x : np.ndarray
Wspólna oś score
pdf_good : np.ndarray
Gęstość dla good
pdf_bad : np.ndarray
Gęstość dla bad
"""
y_true = np.asarray(y_true)
y_score = np.asarray(y_score)
good_scores = y_score[y_true == good_value]
bad_scores = y_score[y_true == bad_value]
if len(good_scores) == 0 or len(bad_scores) == 0:
raise ValueError("Brak obserwacji good lub bad")
x_min = min(good_scores.min(), bad_scores.min())
x_max = max(good_scores.max(), bad_scores.max())
x = np.linspace(x_min, x_max, n_points)
kde_good = gaussian_kde(good_scores, bw_method=bandwidth)
kde_bad = gaussian_kde(bad_scores, bw_method=bandwidth)
pdf_good = kde_good(x)
pdf_bad = kde_bad(x)
return x, pdf_good, pdf_bad
#funkcja do rysowania rozkładów gęstości
def check_gini_vs_good_bad(n_bad, n_good, signal):
y_true, y_score = simulate_scores(n_bad=n_bad, n_good=n_good, signal=signal)
gini_val = -(2 * roc_auc_score(y_true=y_true, y_score=y_score) - 1)
x, pdf_good, pdf_bad = kde_from_scores(y_true=y_true, y_score=y_score, good_value=0, bad_value=1, n_points=400)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=pdf_good, mode='lines+markers', name='Good', line=dict(width=2)))
fig.add_trace(go.Scatter(x=x, y=pdf_bad, mode='lines+markers', name='Bad (default)', line=dict(width=2)))
fig.update_layout(
title=f'Rozkład score dla Good (n={n_good} z N(0,1)) i Bad (n={n_bad} z N({signal},1)). GINI: {gini_val:.2%}',
xaxis_title='Score',
yaxis_title='Liczba obserwacji',
template='plotly_white')
fig.show()
fig = go.Figure()
check_gini_vs_good_bad(400, 2000, 0)
check_gini_vs_good_bad(400, 2000, -0.5)
check_gini_vs_good_bad(400, 2000, -1)
check_gini_vs_good_bad(400, 2000, -1.5)
check_gini_vs_good_bad(400, 2000, -2)